<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let project = null;
let relatedPRDs = [];
let relatedTasks = [];
let relatedDocuments = [];
let relatedDesigns = [];
let relatedTests = [];
let loading = true;
let error = null;
$: projectId = $page.params.id;
onMount(async () => {
await loadProjectData();
});
async function loadProjectData() {
try {
loading = true;
error = null;
// 프로젝트 정보 로드
const projectResponse = await fetch(`/api/projects/${projectId}`);
if (!projectResponse.ok) {
if (projectResponse.status === 404) {
error = '프로젝트를 찾을 수 없습니다.';
} else {
error = '프로젝트 정보를 불러오는 중 오류가 발생했습니다.';
}
return;
}
project = await projectResponse.json();
// project_links 테이블에서 연결된 항목들 로드
const linksResponse = await fetch(`/api/projects/${projectId}/links`);
if (linksResponse.ok) {
const linksData = await linksResponse.json();
if (linksData.success) {
relatedPRDs = linksData.links.prds || [];
relatedTasks = linksData.links.tasks || [];
relatedDocuments = linksData.links.documents || [];
relatedDesigns = linksData.links.designs || [];
relatedTests = linksData.links.tests || [];
}
}
} catch (e) {
error = '데이터를 불러오는 중 오류가 발생했습니다: ' + e.message;
} finally {
loading = false;
}
}
function getStatusText(status) {
const statusMap = {
'active': '활성',
'planning': '계획중',
'on_hold': '보류',
'completed': '완료',
'cancelled': '취소'
};
return statusMap[status] || status;
}
function getPriorityText(priority) {
const priorityMap = {
'high': '높음',
'medium': '보통',
'low': '낮음'
};
return priorityMap[priority] || priority;
}
function getPriorityClass(priority) {
const classMap = {
'high': 'bg-red-100 text-red-800',
'medium': 'bg-yellow-100 text-yellow-800',
'low': 'bg-green-100 text-green-800'
};
return classMap[priority] || 'bg-gray-100 text-gray-800';
}
function getStatusClass(status) {
const classMap = {
'active': 'bg-green-100 text-green-800',
'planning': 'bg-blue-100 text-blue-800',
'on_hold': 'bg-yellow-100 text-yellow-800',
'completed': 'bg-gray-100 text-gray-800',
'cancelled': 'bg-red-100 text-red-800'
};
return classMap[status] || 'bg-gray-100 text-gray-800';
}
function getTaskStatusText(status) {
const statusMap = {
'pending': '대기중',
'in_progress': '진행중',
'completed': '완료',
'blocked': '차단됨'
};
return statusMap[status] || status;
}
function getTaskStatusClass(status) {
const classMap = {
'pending': 'bg-gray-100 text-gray-800',
'in_progress': 'bg-blue-100 text-blue-800',
'completed': 'bg-green-100 text-green-800',
'blocked': 'bg-red-100 text-red-800'
};
return classMap[status] || 'bg-gray-100 text-gray-800';
}
function getDocumentStatusText(status) {
const statusMap = {
'draft': '초안',
'review': '검토중',
'approved': '승인됨',
'archived': '보관됨'
};
return statusMap[status] || status;
}
function getDocumentStatusClass(status) {
const classMap = {
'draft': 'bg-gray-100 text-gray-800',
'review': 'bg-yellow-100 text-yellow-800',
'approved': 'bg-green-100 text-green-800',
'archived': 'bg-blue-100 text-blue-800'
};
return classMap[status] || 'bg-gray-100 text-gray-800';
}
function getTestStatusText(status) {
const statusMap = {
'draft': '초안',
'ready': '준비',
'active': '활성',
'deprecated': '비활성'
};
return statusMap[status] || status;
}
function getTestStatusClass(status) {
const classMap = {
'draft': 'bg-gray-100 text-gray-800',
'ready': 'bg-blue-100 text-blue-800',
'active': 'bg-green-100 text-green-800',
'deprecated': 'bg-red-100 text-red-800'
};
return classMap[status] || 'bg-gray-100 text-gray-800';
}
function getDesignStatusText(status) {
const statusMap = {
'draft': '초안',
'review': '검토중',
'approved': '승인',
'implemented': '구현됨'
};
return statusMap[status] || status;
}
function getDesignStatusClass(status) {
const classMap = {
'draft': 'bg-gray-100 text-gray-800',
'review': 'bg-yellow-100 text-yellow-800',
'approved': 'bg-green-100 text-green-800',
'implemented': 'bg-blue-100 text-blue-800'
};
return classMap[status] || 'bg-gray-100 text-gray-800';
}
function getDesignTypeText(designType) {
const typeMap = {
'system': '시스템',
'architecture': '아키텍처',
'ui_ux': 'UI/UX',
'database': '데이터베이스',
'api': 'API'
};
return typeMap[designType] || designType;
}
function getDocumentTypeText(docType) {
const typeMap = {
'test_guide': '테스트 가이드',
'test_results': '테스트 결과',
'analysis': '분석',
'report': '보고서',
'checklist': '체크리스트',
'specification': '사양서',
'meeting_notes': '회의록',
'decision_log': '의사결정'
};
return typeMap[docType] || docType;
}
// 진행률 계산
$: completedTasks = relatedTasks.filter(task => task.status === 'completed').length;
$: totalTasks = relatedTasks.length;
$: progressPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
</script>
<svelte:head>
<title>{project?.name || '프로젝트'} - WorkflowMCP</title>
</svelte:head>
<div class="max-w-6xl mx-auto space-y-6">
{#if loading}
<div class="flex justify-center items-center h-64">
<div class="text-gray-600">프로젝트 정보를 불러오는 중...</div>
</div>
{:else if error}
<div class="bg-red-50 border border-red-200 rounded-md p-4">
<div class="text-red-800">{error}</div>
<div class="mt-2">
<a href="/projects" class="text-red-600 hover:text-red-800 underline">
프로젝트 목록으로 돌아가기
</a>
</div>
</div>
{:else if project}
<!-- 프로젝트 헤더 -->
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center space-x-3 mb-2">
<h1 class="text-3xl font-bold text-gray-900">{project.name}</h1>
<span class="badge {getStatusClass(project.status)}">
{getStatusText(project.status)}
</span>
<span class="badge {getPriorityClass(project.priority)}">
{getPriorityText(project.priority)}
</span>
</div>
{#if project.description}
<p class="text-gray-600 text-lg">{project.description}</p>
{/if}
</div>
<div class="flex space-x-3">
<a href="/projects/{projectId}/edit" class="btn btn-secondary">
편집
</a>
<a href="/projects" class="btn btn-secondary">
← 목록으로
</a>
</div>
</div>
<!-- 프로젝트 정보 카드 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 기본 정보 -->
<div class="card">
<h2 class="text-xl font-semibold text-gray-900 mb-4">프로젝트 정보</h2>
<div class="space-y-3">
<div>
<dt class="text-sm font-medium text-gray-500">관리자</dt>
<dd class="text-sm text-gray-900">{project.manager || '미지정'}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">시작일</dt>
<dd class="text-sm text-gray-900">
{project.start_date ? new Date(project.start_date).toLocaleDateString('ko-KR') : '미정'}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">종료일</dt>
<dd class="text-sm text-gray-900">
{project.end_date ? new Date(project.end_date).toLocaleDateString('ko-KR') : '미정'}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">생성일</dt>
<dd class="text-sm text-gray-900">
{new Date(project.created_at).toLocaleDateString('ko-KR')}
</dd>
</div>
{#if project.tags && project.tags.length > 0}
<div>
<dt class="text-sm font-medium text-gray-500 mb-1">태그</dt>
<dd class="flex flex-wrap gap-1">
{#each project.tags as tag}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{tag}
</span>
{/each}
</dd>
</div>
{/if}
</div>
</div>
<!-- 진행 상황 -->
<div class="card">
<h2 class="text-xl font-semibold text-gray-900 mb-4">진행 상황</h2>
<div class="space-y-4">
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">전체 진행률</span>
<span class="text-sm text-gray-600">{progressPercentage}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
style="width: {progressPercentage}%"
></div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">{completedTasks}</div>
<div class="text-sm text-gray-600">완료된 작업</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-gray-600">{totalTasks}</div>
<div class="text-sm text-gray-600">전체 작업</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="text-center">
<div class="text-xl font-bold text-green-600">{relatedPRDs.length}</div>
<div class="text-sm text-gray-600">연결된 PRD</div>
</div>
<div class="text-center">
<div class="text-xl font-bold text-purple-600">{relatedDocuments.length}</div>
<div class="text-sm text-gray-600">연결된 문서</div>
</div>
</div>
</div>
</div>
<!-- 메모 -->
<div class="card">
<h2 class="text-xl font-semibold text-gray-900 mb-4">메모</h2>
<div class="text-sm text-gray-600">
{#if project.notes}
<pre class="whitespace-pre-wrap">{project.notes}</pre>
{:else}
<span class="text-gray-400">메모가 없습니다.</span>
{/if}
</div>
</div>
</div>
<!-- 연결된 PRD -->
{#if relatedPRDs.length > 0}
<div class="card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">연결된 PRD ({relatedPRDs.length}개)</h2>
<a href="/prds/new" class="btn btn-primary btn-sm">
새 PRD 추가
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each relatedPRDs as prd}
<div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors">
<div class="flex items-start justify-between mb-2">
<h3 class="font-medium text-gray-900 truncate">{prd.title}</h3>
<span class="badge {getStatusClass(prd.status)} ml-2">
{getStatusText(prd.status)}
</span>
</div>
{#if prd.description}
<p class="text-sm text-gray-600 mb-3 line-clamp-2">{prd.description}</p>
{/if}
<div class="flex items-center justify-between">
<span class="badge {getPriorityClass(prd.priority)}">
{getPriorityText(prd.priority)}
</span>
<a href="/prds/{prd.entity_id || prd.id}" class="text-blue-600 hover:text-blue-800 text-sm">
상세보기
</a>
</div>
</div>
{/each}
</div>
</div>
{/if}
<!-- 연결된 작업 -->
{#if relatedTasks.length > 0}
<div class="card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">연결된 작업 ({relatedTasks.length}개)</h2>
<a href="/tasks/new" class="btn btn-primary btn-sm">
새 작업 추가
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each relatedTasks as task}
<div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors">
<div class="flex items-start justify-between mb-2">
<h3 class="font-medium text-gray-900 truncate">{task.title}</h3>
<span class="badge {getTaskStatusClass(task.status)} ml-2">
{getTaskStatusText(task.status)}
</span>
</div>
{#if task.description}
<p class="text-sm text-gray-600 mb-3 line-clamp-2">{task.description}</p>
{/if}
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="badge {getPriorityClass(task.priority)}">
{getPriorityText(task.priority)}
</span>
{#if task.assignee}
<span class="text-xs text-gray-500">
👤 {task.assignee}
</span>
{/if}
</div>
<a href="/tasks/{task.entity_id || task.id}" class="text-blue-600 hover:text-blue-800 text-sm">
상세보기
</a>
</div>
{#if task.due_date}
<div class="text-xs text-gray-500 mt-2">
📅 {new Date(task.due_date).toLocaleDateString('ko-KR')}
</div>
{/if}
</div>
{/each}
</div>
</div>
{:else}
<div class="card">
<div class="text-center py-8">
<div class="text-gray-400 mb-4">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
</div>
<p class="text-gray-600 mb-4">아직 연결된 작업이 없습니다.</p>
<a href="/tasks/new" class="btn btn-primary">
첫 번째 작업 추가하기
</a>
</div>
</div>
{/if}
<!-- 연결된 문서 -->
{#if relatedDocuments.length > 0}
<div class="card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">연결된 문서 ({relatedDocuments.length}개)</h2>
<a href="/documents" class="btn btn-secondary btn-sm">
문서 관리
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each relatedDocuments as document}
<div class="bg-purple-50 rounded-lg p-4 hover:bg-purple-100 transition-colors border border-purple-200">
<div class="flex items-start justify-between mb-2">
<h3 class="font-medium text-gray-900 truncate">{document.title}</h3>
<span class="badge {getDocumentStatusClass(document.status)} ml-2">
{getDocumentStatusText(document.status)}
</span>
</div>
{#if document.summary}
<p class="text-sm text-gray-600 mb-3 line-clamp-2">{document.summary}</p>
{/if}
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="badge bg-purple-100 text-purple-800">
📋 {getDocumentTypeText(document.doc_type)}
</span>
</div>
<a href="/documents/{document.entity_id || document.id}" class="text-purple-600 hover:text-purple-800 text-sm">
상세보기
</a>
</div>
{#if document.linked_at}
<div class="text-xs text-gray-500 mt-2">
🔗 {new Date(document.linked_at).toLocaleDateString('ko-KR')}
</div>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- 연결된 설계 -->
{#if relatedDesigns.length > 0}
<div class="card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">연결된 설계 ({relatedDesigns.length}개)</h2>
<a href="/designs/new" class="btn btn-primary btn-sm">
새 설계 추가
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each relatedDesigns as design}
<div class="bg-green-50 rounded-lg p-4 hover:bg-green-100 transition-colors border border-green-200">
<div class="flex items-start justify-between mb-2">
<h3 class="font-medium text-gray-900 truncate">{design.title}</h3>
<span class="badge {getDesignStatusClass(design.status)} ml-2">
{getDesignStatusText(design.status)}
</span>
</div>
{#if design.description}
<p class="text-sm text-gray-600 mb-3 line-clamp-2">{design.description}</p>
{/if}
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="badge {getPriorityClass(design.priority)}">
{getPriorityText(design.priority)}
</span>
{#if design.design_type}
<span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
{design.design_type === 'system' ? '시스템' : design.design_type === 'architecture' ? '아키텍처' : design.design_type === 'ui_ux' ? 'UI/UX' : design.design_type === 'database' ? '데이터베이스' : design.design_type === 'api' ? 'API' : design.design_type}
</span>
{/if}
</div>
</div>
<div class="mt-3 flex items-center justify-between">
<a href="/designs/{design.entity_id || design.id}" class="text-green-600 hover:text-green-800 text-sm">
상세보기 →
</a>
</div>
{#if design.linked_at}
<div class="text-xs text-gray-500 mt-2">
🔗 {new Date(design.linked_at).toLocaleDateString('ko-KR')}
</div>
{/if}
</div>
{/each}
</div>
</div>
{:else}
<div class="card text-center py-8">
<div class="text-gray-400 mb-4">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
</div>
<p class="text-gray-600 mb-4">아직 연결된 설계가 없습니다.</p>
<a href="/designs/new" class="btn btn-primary">
첫 번째 설계 추가하기
</a>
</div>
{/if}
<!-- 연결된 테스트 -->
{#if relatedTests.length > 0}
<div class="card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-900">연결된 테스트 ({relatedTests.length}개)</h2>
<a href="/tests/new" class="btn btn-primary btn-sm">
새 테스트 추가
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each relatedTests as test}
<div class="bg-blue-50 rounded-lg p-4 hover:bg-blue-100 transition-colors border border-blue-200">
<div class="flex items-start justify-between mb-2">
<h3 class="font-medium text-gray-900 truncate">{test.title}</h3>
<span class="badge {getTestStatusClass(test.status)} ml-2">
{getTestStatusText(test.status)}
</span>
</div>
{#if test.description}
<p class="text-sm text-gray-600 mb-3 line-clamp-2">{test.description}</p>
{/if}
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="badge {getPriorityClass(test.priority)}">
{getPriorityText(test.priority)}
</span>
{#if test.type}
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{test.type}
</span>
{/if}
</div>
<div class="flex items-center text-xs text-gray-500 space-x-2">
{#if test.estimated_duration}
<span>⏱️ {test.estimated_duration}분</span>
{/if}
</div>
</div>
<div class="mt-3 flex items-center justify-between">
<a href="/tests/{test.id}" class="text-blue-600 hover:text-blue-800 text-sm">
상세보기 →
</a>
</div>
</div>
{/each}
</div>
</div>
{:else}
<div class="card text-center py-8">
<div class="text-gray-400 mb-4">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<p class="text-gray-600 mb-4">아직 연결된 테스트가 없습니다.</p>
<a href="/tests/new" class="btn btn-primary">
첫 번째 테스트 추가하기
</a>
</div>
{/if}
{/if}
</div>
<style>
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 500;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>